/*
 * Created on Feb 12, 2005
 * Created by dfhuynh
 */
package edu.mit.simile.longwell.schema.impl;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import com.hp.hpl.jena.datatypes.TypeMapper;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.NodeIterator;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.vocabulary.DC;
import com.hp.hpl.jena.vocabulary.RDF;
import com.hp.hpl.jena.vocabulary.RDFS;
import com.hp.hpl.jena.vocabulary.XSD;

import edu.mit.simile.SimileUtilities;
import edu.mit.simile.longwell.LongwellServer;
import edu.mit.simile.longwell.Profile;
import edu.mit.simile.longwell.schema.ISchemaModel;
import edu.mit.simile.longwell.schema.IStructuredProperty;

/**
 * @author dfhuynh
 */
class StructuredProperty implements IStructuredProperty {
	final static private Logger s_logger = 
		Logger.getLogger(StructuredProperty.class);
	
	static protected Map 	s_uriToRecord = new HashMap();
	static protected Map 	s_uriToProfileToInstance = new HashMap();
	
	static protected Map	m_propertyLabelCache = new HashMap();
	static protected Map	m_typeLabelPropertyCache = new HashMap();
	
	static class Record {
		String		m_uri;
		boolean		m_explicit;
		
		boolean[]	m_typeFlags = new boolean[IStructuredProperty.s_type_max];
		long[]		m_typeCounts = new long[IStructuredProperty.s_type_max];
		long		m_totalCount;
		long		m_totalUniqueCount;
	}
	
	final protected Profile	m_profile;
	final protected Record	m_record;
	
	protected long 	m_occurrences;
	protected long	m_uniqueOccurrences;
	
	StructuredProperty(Profile profile, Record record) {
		m_profile = profile;
		m_record = record;
	}
	
	static public void dispose(Profile profile) {
		synchronized (s_uriToProfileToInstance) {
			Iterator i = s_uriToProfileToInstance.values().iterator();
			while (i.hasNext()) {
				Map profileToInstance = (Map) i.next();
				profileToInstance.remove(profile);
			}
		}
	}
	
	static void load(File file) {
		if (!file.exists()) {
			return;
		}
		
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			
			ObjectInputStream ois = new ObjectInputStream(fis);
			
			int count = ois.readInt();
			
			for (int i = 0; i < count; i++) {
				Record record = getRecord((String) ois.readObject());

				record.m_explicit = ois.readBoolean();

				int flagCount = ois.readInt();
				record.m_typeFlags = new boolean[flagCount];
				for (int j = 0; j < flagCount; j++) {
					record.m_typeFlags[j] = ois.readBoolean();
				}
				
				int typeCount = ois.readInt();
				record.m_typeCounts = new long[typeCount];
				for (int j = 0; j < typeCount; j++) {
					record.m_typeCounts[j] = ois.readLong();
				}

				record.m_totalCount = ois.readLong();
				record.m_totalUniqueCount = ois.readLong();
			}
		} catch (EOFException e) {
			s_logger.warn("BUG: We should not get an EOFException. Ignoring for now.");
		} catch (Exception e) {
			s_logger.error(
				"Failed to read properties from schema registry",
				e);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e1) {
				}
			}
		}
	}

	static void save(File file) {
		synchronized (s_uriToRecord) {
			FileOutputStream fos = null;
			ObjectOutputStream oos = null;
			try {
				fos = new FileOutputStream(file);
				oos = new ObjectOutputStream(fos);
				
				Set records = new HashSet(s_uriToRecord.values());
				
				oos.writeInt(records.size());
	
				Iterator i = records.iterator();
				while (i.hasNext()) {
					Record record = (Record) i.next();
					
					oos.writeObject(record.m_uri);
					oos.writeBoolean(record.m_explicit);
	
					oos.writeInt(record.m_typeFlags.length);
					for (int j = 0; j < record.m_typeFlags.length; j++) {
						oos.writeBoolean(record.m_typeFlags[j]);
					}
	
					oos.writeInt(record.m_typeCounts.length);
					for (int j = 0; j < record.m_typeCounts.length; j++) {
						oos.writeLong(record.m_typeCounts[j]);
					}
	
					oos.writeLong(record.m_totalCount);
					oos.writeLong(record.m_totalUniqueCount);
				}
			} catch (Exception e) {
				s_logger.error(
					"Failed to write properties to schema registry",
					e);
			} finally {
				if (oos != null) {
					try {
						oos.close();
					} catch (IOException e) {
					}
				}
				if (fos != null) {
					try {
						fos.close();
					} catch (IOException e) {
					}
				}
			}
		}
	}

	static void load(File file, Profile profile) {
		if (!file.exists()) {
			return;
		}
		
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			
			ObjectInputStream ois = new ObjectInputStream(fis);
			
			Model model = profile.getModel();
			ISchemaModel schemaModel = profile.getSchemaModel();
			
			int count = ois.readInt();
			
			for (int i = 0; i < count; i++) {
				StructuredProperty sProperty = 
					getStructuredProperty(
						profile, 
						model.createProperty((String) ois.readObject()),
						true
					);
				
				sProperty.m_occurrences = ois.readLong();
				sProperty.m_uniqueOccurrences = ois.readLong();
			}
		} catch (EOFException e) {
			s_logger.warn("BUG: We should not get an EOFException. Ignoring for now.");
		} catch (Exception e) {
			s_logger.error(
				"Failed to read profile specific properties from schema registry",
				e);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e1) {
				}
			}
		}
	}

	static void save(File file, Profile profile) {
		synchronized (s_uriToProfileToInstance) {
			FileOutputStream fos = null;
			ObjectOutputStream oos = null;
			try {
				fos = new FileOutputStream(file);
				oos = new ObjectOutputStream(fos);
				
				Model model = profile.getModel();
				ISchemaModel schemaModel = profile.getSchemaModel();
				
				Set instances = new HashSet();
				
				Iterator i = s_uriToProfileToInstance.values().iterator();
				while (i.hasNext()) {
					Map profileToInstance = (Map) i.next();
					StructuredProperty sProperty = 
						(StructuredProperty) profileToInstance.get(profile);
					
					if (sProperty != null) {
						instances.add(sProperty);
					}
				}
				
				oos.writeInt(instances.size());
	
				i = instances.iterator();
				while (i.hasNext()) {
					StructuredProperty sProperty = (StructuredProperty) i.next();
					
					oos.writeObject(sProperty.m_record.m_uri);
					oos.writeLong(sProperty.m_occurrences);
					oos.writeLong(sProperty.m_uniqueOccurrences);
				}
			} catch (Exception e) {
				s_logger.error(
					"Failed to write profile specific properties to schema registry",
					e);
			} finally {
				if (oos != null) {
					try {
						oos.close();
					} catch (IOException e) {
					}
				}
				if (fos != null) {
					try {
						fos.close();
					} catch (IOException e) {
					}
				}
			}
		}
	}
	
	static StructuredProperty getStructuredProperty(
		Profile profile, Property property, boolean loadingFromFile) {
		
		synchronized (s_uriToProfileToInstance) {
			Map profileToInstance = (Map) s_uriToProfileToInstance.get(property.getURI());
			if (profileToInstance == null) {
				profileToInstance = new HashMap();
				s_uriToProfileToInstance.put(property.getURI(), profileToInstance);
			}
			
			StructuredProperty instance = (StructuredProperty)
				profileToInstance.get(profile);
			if (instance == null) {
				String propertyURI = property.getURI();
				if (!propertyURI.startsWith(RDF.getURI() + "_")) {
					Record record = getRecord(property.getURI());
					
					instance = new StructuredProperty(profile, record);
					if (profile.getModel().contains(property, RDF.type, (RDFNode) null)) {
						getExplicitPropertyMetadata(instance.m_record, profile);
					}
					
					profileToInstance.put(profile, instance);
					
					if (!loadingFromFile) {
						instance.onDataAdded(profile.getModel());
					}
				}
			}
		
			return instance;
		}
	}
	
	synchronized public void onDataAdded(Model model) {
		final String[] protocolPrefixes = new String[] {
			// TODO where can we get the real list of protocol prefixes?
			"http:", "https:", "ftp:", "news:", "file:", "mailto:"
		};
		
		long[] counts = m_record.m_typeCounts;
		
		long occurrences = 0;
		long uniqueOccurrences = 0;
		
		Property p = model.createProperty(m_record.m_uri);
		
		StmtIterator si = model.listStatements(null, p, (RDFNode) null);
		while (si.hasNext()) {
			si.next();
			occurrences++;
		}
		
		NodeIterator ni = model.listObjectsOfProperty(p);
		while (ni.hasNext()) {
			RDFNode node = ni.nextNode();
			
			uniqueOccurrences++;
			
			if (node instanceof Literal) {
				counts[s_type_literal]++;
				
				String s = ((Literal) node).getString();

				parse:while (true) {
					try {
						Integer.parseInt(s);
						
						counts[s_type_integer]++;
						counts[s_type_numeric]++;
						
						break parse;
					} catch (NumberFormatException e) {
					}
					
					if ("true".equals(s) || "false".equals(s)) {
						counts[s_type_boolean]++;
						break parse;
					}
					
					for (int i = 0; i < protocolPrefixes.length; i++) {
						if (s.startsWith(protocolPrefixes[i])) {
							counts[s_type_uri]++;
							break parse;
						}
					}
					
					if (SimileUtilities.parseDate(s) != null) {
						counts[s_type_dateTime]++;
						break parse;
					}
					
					break;
				}
			}
		}
		
		m_record.m_totalCount += occurrences;
		m_record.m_totalUniqueCount += uniqueOccurrences;
		
		m_occurrences += occurrences;
		m_uniqueOccurrences += uniqueOccurrences;
	}
	
	synchronized public void onDataRemoved(Model model) {
		NodeIterator ni = 
			model.listObjectsOfProperty(
				model.createProperty(m_record.m_uri));
		
		while (ni.hasNext()) {
			ni.nextNode();
			
			m_occurrences--;
		}
	}
	
	static protected Record getRecord(String uri) {
		synchronized (s_uriToRecord) {
			Record record = (Record) s_uriToRecord.get(uri);
			if (record == null) {
				record = new Record();
				record.m_uri = uri;
				
				s_uriToRecord.put(uri, record);
			}
			return record;
		}
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getURI()
	 */
	synchronized public String getURI() {
		return m_record.m_uri;
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#isExplicit()
	 */
	synchronized public boolean isExplicit() {
		return m_record.m_explicit;
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getDomainClasses()
	 */
	synchronized public Set getDomainClasses() {
		return StructuredClass.guessStructuredClassesForProperty(
				m_profile, m_record.m_uri);
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getSchemas()
	 */
	synchronized public Set getSchemas() {
		return StructuredSchema.guessStructuredSchemas(m_profile, m_record.m_uri);
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getLabel(java.lang.String)
	 */
	synchronized public String getLabel(String locale) {
		return getPropertyLabel(m_profile.getModel(), m_record.m_uri, locale, true);
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getFrequency()
	 */
	synchronized public long countOccurrences() {
		return m_occurrences;
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getUniqueness()
	 */
	synchronized public float getUniqueness() {
		return m_uniqueOccurrences / (float) m_occurrences;
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.model.IStructuredProperty#getTypeConfidence(int)
	 */
	synchronized public float getTypeConfidence(int type) {
		if (type < 0 || type >= s_type_max) {
			return 0;
		} else {
			return m_record.m_explicit ?
				calculateConfidence(
					m_record.m_typeFlags[type],
					m_record.m_typeCounts[type],
					m_record.m_totalUniqueCount) :
				calculateConfidence(
					m_record.m_typeCounts[type],
					m_record.m_totalUniqueCount);
		}
	}
	
	static protected float calculateConfidence(long count, long total) {
		// TODO 5 is a magic number for now
		return count / (total + 5f);
	}

	static protected float calculateConfidence(boolean flag, long count, long total) {
		return flag ? 1f : calculateConfidence(count, total);
	}

	static private void getExplicitPropertyMetadata(Record record, Profile profile) {
		record.m_explicit = true;
		
		Model	model = profile.getModel();
		Model	model2 = LongwellServer.getServer().getDefaultProfile().getModel();
		String 	uri = record.m_uri;

		// Is Literal?
		{
			Statement s1 = model.getProperty(
				model.createResource(record.m_uri), RDFS.range);
			Statement s2 = model == model2 ? 
				null :
				model.getProperty(model.createResource(record.m_uri), RDFS.range);

			if (s1 == null && s2 == null) {
				record.m_typeFlags[s_type_literal] = true;
			} else if (s1 != null) {
				record.m_typeFlags[s_type_literal] = 
					TypeMapper.getInstance().getTypeByName(
						s1.getResource().getURI()) != null;
			} else if (s2 != null) {
				record.m_typeFlags[s_type_literal] = 
					TypeMapper.getInstance().getTypeByName(
						s2.getResource().getURI()) != null;
			}
		}
		if (!record.m_typeFlags[s_type_literal]) {
			return; // we're done
		}
		
		// Is date-time?
		{
			if (propertyIsSubPropertyOf(model, uri, new Property[] { DC.date }) ||
				(model != model2 && 
					propertyIsSubPropertyOf(model2, uri, new Property[] { DC.date }))) {
				record.m_typeFlags[s_type_dateTime] = true;
			} else {
				Resource type = getPropertyType(model, uri);
				if (type == null && model != model2) {
					type = getPropertyType(model2, uri);
				}
				
				if (XSD.date.equals(type) || XSD.dateTime.equals(type)) {
					record.m_typeFlags[s_type_dateTime] = true;
				}
			}
		}
		if (record.m_typeFlags[s_type_dateTime]) {
			return; // we're done
		}
		
		// Is integer?
		{
			final Resource[] integerTypes = new Resource[] {
				XSD.integer, 
				XSD.negativeInteger, XSD.nonNegativeInteger, 
				XSD.positiveInteger, XSD.nonPositiveInteger,
				
				XSD.unsignedByte, 
				XSD.unsignedInt, XSD.unsignedLong, XSD.unsignedShort,
				
				XSD.xbyte, XSD.xint, XSD.xlong, XSD.xshort
			};
			
			Resource type = getPropertyType(model, uri);
			if (type == null && model != model2) {
				type = getPropertyType(model2, uri);
			}
			
			if (type != null) {
				for (int i = 0; i < integerTypes.length; i++) {
					if (integerTypes[i].equals(type)) {
						record.m_typeFlags[s_type_integer] = true;
						break;
					}
				}
			}
		}
		
		// Is numeric?
		if (record.m_typeFlags[s_type_integer]) {
			record.m_typeFlags[s_type_numeric] = true;
		} else {
			final Resource[] numericTypes = new Resource[] {
				XSD.decimal, XSD.integer, 
				XSD.negativeInteger, XSD.nonNegativeInteger, 
				XSD.positiveInteger, XSD.nonPositiveInteger,
				
				XSD.unsignedByte, 
				XSD.unsignedInt, XSD.unsignedLong, XSD.unsignedShort,
				
				XSD.xbyte, XSD.xdouble, XSD.xfloat, XSD.xint, XSD.xlong, XSD.xshort
			};
			
			Resource type = getPropertyType(model, record.m_uri);
			if (type == null && model != model2) {
				type = getPropertyType(model2, record.m_uri);
			}
			
			if (type != null) {
				for (int i = 0; i < numericTypes.length; i++) {
					if (numericTypes[i].equals(type)) {
						record.m_typeFlags[s_type_numeric] = true;
						break;
					}
				}
			}
		}
		if (record.m_typeFlags[s_type_numeric]) {
			return; // we're done
		}
		
		// Is boolean?
		{
			final Resource[] booleanTypes = new Resource[] {
				XSD.xboolean
			};
			
			Resource type = getPropertyType(model, record.m_uri);
			if (type == null && model != model2) {
				type = getPropertyType(model2, record.m_uri);
			}
			
			if (type != null) {
				for (int i = 0; i < booleanTypes.length; i++) {
					if (booleanTypes[i].equals(type)) {
						record.m_typeFlags[s_type_boolean] = true;
						break;
					}
				}
			}
		}
	}

	static protected String getPropertyLabel(
			Model model, String propertyURI, String locale, boolean abbreviate) {
		Property 	property = model.createProperty(propertyURI);
		String 		label = (String) m_propertyLabelCache.get(propertyURI);
		
		if (label == null) {
			Statement s = model.getProperty(property, RDFS.label);
			if (s != null) {
				label = s.getString();
			}
			
			if (label == null) {
				s = model.getProperty(property, DC.title);
				if (s != null) {
					label = s.getString();
				}
			}
			
			if (label == null) {
				if (abbreviate) {
					label = SimileUtilities.abbreviateURI(propertyURI);
				} else {
					label = propertyURI;
				}
			}
			
			m_propertyLabelCache.put(propertyURI, label);
		}
		
		return label;
	}

	static protected Resource getPropertyType(Model model, String uri) {
		Property p = model.createProperty(uri);
		while (true) {
			Statement s = model.getProperty(p, RDFS.range);
			if (s != null) {
				return (Resource) s.getObject();
			}
			
			s = model.getProperty(p, RDFS.subPropertyOf);
			if (s != null) {
				RDFNode n = s.getObject();
				if (n instanceof Resource) {
					p = model.createProperty(((Resource) n).getURI());
					continue;
				}
			}
			
			break;
		}
		
		return null;
	}
	
	static protected boolean propertyIsSubPropertyOf(
			Model model, String uri, Property[] properties) {
		Property p = model.createProperty(uri);
		while (true) {
			for (int i = 0; i < properties.length; i++) {
				if (properties[i].equals(p)) {
					return true;
				}
			}
			
			Statement s = model.getProperty(p, RDFS.subPropertyOf);
			if (s != null) {
				RDFNode n = s.getObject();
				if (n instanceof Resource) {
					p = model.createProperty(((Resource) n).getURI());
					continue;
				}
			}
			
			break;
		}
		
		return false;
	}
	
}
